﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Week1.world.behavior;
using Week1.player;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Week1.world.terrain
{
    /// <summary>
    /// contains grids, each cell is 10 x 10 units. Each cell contains 4 object references allowing 4 game objects of sizxe 5x5 
    /// to occupy the same cell.
    /// If more then 4 objects try to register to the same cell, grid notify the game objec to move to other cell using move behavior.
    /// Grid size is fixed to 30 X 30 max cells. That equals 300 X 300 3d pixels and 120 x 120 ground units.
    /// Land always occupy 1 entire cell unlike units that can take as less as 1/4th of the cell. Some units can take entire cell
    /// or more than one cell.
    /// </summary>
    public class GameGrid
    {
        // TODO: Create unregister
        private static GameGrid instance;

        #region grid variables
        public const int GRIDSIZE = 20;
        public const int GRIDUNITSIZE = 10; // size of each cell is 10 units. 
        public const int XSTART = - GRIDSIZE * GRIDUNITSIZE; //  x offset in world coordinates for cell at x index 0
        public const int YSTART = XSTART; // y offset in world coordinates for cell at y index 0

        enum LandLevel : int
        {
            lvl1, lvl2, // open path with at different levels
            downrampnorth, downrampeast, downrampsouth, downrampwest, // ramps to different levels
            block // cell is completely blocked and level is undetermined
        };
        enum LandPath : byte
        {
            goNorth, goEast, goSouth, goWest
        };

        struct Cell
        {
            public DrawableGlobalVar unit1;
            public DrawableGlobalVar unit2;
            public DrawableGlobalVar unit3;
            public DrawableGlobalVar unit4;
            public byte unitsRegistered; // contains how many units are registered in this cell. max 4.
            public LandPath landPath; // contains directions to destination
            public LandLevel landType; // contains land type. see enum landType
        }

        Cell[][] oGrid;
        Cell[][] oAirGrid;

        #endregion

        #region debug variables
        //TODO: Move render debug to a different drawable class that woudl extend DrawPrimitive. 
        protected VertexPositionColor[] oRenderGrid;
        BasicEffect oEffect;
        GraphicsDevice oDevice;
        Camera oCamera;
        DepthStencilState oDepthState;
        int iGridVertexIDx = 0;
        #endregion

        private GameGrid(GraphicsDevice device, Week1.player.Camera camera)
        {

            oCamera = camera;
            oDevice = device;

            // initialize grid to 200x200 size.
            oGrid = new Cell[GRIDSIZE][];
            oAirGrid = new Cell[GRIDSIZE][];

            // each cell requires 4 lines to render during debug + 4 lines to render air grid.
            oRenderGrid = new VertexPositionColor[GRIDSIZE + 400];

            oEffect = new BasicEffect(oDevice);

            for (int iDx = 0; iDx < GRIDSIZE; iDx++)
            {
                oGrid[iDx] = new Cell[GRIDSIZE];
            }

            oGrid[0][0].landType = LandLevel.downrampwest;
            oGrid[0][1].landType = LandLevel.lvl2;
            oGrid[0][2].landType = LandLevel.downrampeast;
            oGrid[1][1].landType = LandLevel.downrampnorth;

            oGrid[5][4].landType = LandLevel.downrampwest;
            oGrid[6][4].landType = LandLevel.downrampwest;
            oGrid[5][5].landType = LandLevel.lvl2;
            oGrid[5][6].landType = LandLevel.lvl2;
            oGrid[6][5].landType = LandLevel.lvl2;
            oGrid[7][7].landType = LandLevel.block;
            oGrid[7][8].landType = LandLevel.block;

            oGrid[9][8].landType = LandLevel.lvl2;
            oGrid[9][9].landType = LandLevel.lvl2;

            oDepthState = new DepthStencilState();
            oDepthState.DepthBufferEnable = true;

            initDebugGridRender();
        } 

        #region grid visualization code ----------------------------------------------------
        private struct Debugcell
        {
            int Offset;
            public Vector3 Start1;
            public Vector3 Start2;
            public Vector3 End1;
            public Vector3 End2;
            public void setStartPoint(int Row, int Col, LandLevel landType, int rowScan, int colScan)
            {
                Offset = -(GRIDSIZE * GRIDUNITSIZE) / 2;
                // y = rows, x = cols
                int zLvl = (int)landType * GRIDUNITSIZE / 2;
                Start1.X = Col * GRIDUNITSIZE + Offset;
                Start1.Y = Row * GRIDUNITSIZE + Offset;
                Start1.Z = zLvl;

                Start2.X = Col * GRIDUNITSIZE + Offset + colScan * GRIDUNITSIZE;
                Start2.Y = Row * GRIDUNITSIZE + Offset + rowScan * GRIDUNITSIZE;
                Start2.Z = zLvl;

            }
            public void setEndPoint(int Row, int Col, LandLevel landType, int rowScan, int colScan)
            {
                Offset = -(GRIDSIZE * GRIDUNITSIZE) / 2;
                int zLvl = (int)landType * GRIDUNITSIZE / 2;

                End1.X = Col * GRIDUNITSIZE + Offset;
                End1.Y = Row * GRIDUNITSIZE + Offset;
                End1.Z = zLvl;

                End2.X = Col * GRIDUNITSIZE + Offset + colScan * GRIDUNITSIZE;
                End2.Y = Row * GRIDUNITSIZE + Offset + rowScan * GRIDUNITSIZE;
                End2.Z = zLvl;
            }
        }
        public Vector3 getCellCenteredCoordinates(int cellX, int cellY)
        {
            int gridUnitMid = GRIDUNITSIZE / 2;
            int offset = -(GRIDSIZE * GRIDUNITSIZE) / 2 + gridUnitMid;
            int landlvl = (int) oGrid[cellX][cellY].landType;
            landlvl = landlvl > 1 ? 1 : 0;
            Vector3 position = new Vector3();
            
            position.X = cellX * GRIDUNITSIZE + offset;
            position.Y = cellY * GRIDUNITSIZE + offset;
            position.Z = landlvl / 2;
            
            return position;

        }
        private void initDebugGridRender()
        {
            Debugcell debugCell = new Debugcell();
            Color gridColor = Color.Green;

            setRows(ref debugCell, ref gridColor);
            setCols(ref debugCell, ref gridColor);

        }
        private void setRows(ref Debugcell debugCell, ref Color gridColor)
        {
            LandLevel currLand;
            LandLevel prevLand;
            Debugcell oRampCell = new Debugcell();

            for (int row = 0; row < GRIDSIZE; row++)
            {
                prevLand = oGrid[row][0].landType;
                prevLand = (int)prevLand > 1 ? 0 : prevLand;

                currLand = oGrid[row][0].landType;
                // check if curr land type is ramp
                if ((int)currLand > 1)
                {
                    setEastWestRamp(row, 0, currLand, gridColor);
                    currLand = LandLevel.lvl1;
                }

                // set start point at col 0
                debugCell.setStartPoint(row, 0, prevLand, 1, 0);

                for (int col = 1; col < GRIDSIZE; col++)
                {
                    currLand = oGrid[row][col].landType;
                    if ((int)currLand > 1)
                    {
                        if (currLand == LandLevel.block)
                            setBlockLand(row, col);
                        else
                            setEastWestRamp(row, col, currLand, gridColor);
                        currLand = LandLevel.lvl1;
                    }

                    if (currLand != prevLand)
                    {
                        connectLevels(row, col, gridColor, 1, 0);
                        // set end point
                        debugCell.setEndPoint(row, col, prevLand, 1, 0);
                        setGridVerts(ref debugCell, gridColor);

                        // set new start point at new level
                        debugCell.setStartPoint(row, col, currLand, 1, 0);

                        prevLand = currLand;
                    }// if land type changed
                }// Cols
                // set end point at the end of the cols
                debugCell.setEndPoint(row, GRIDSIZE, currLand, 1, 0);
                setGridVerts(ref debugCell, gridColor);
            }// rows
        }
        private void setCols(ref Debugcell debugCell, ref Color gridColor)
        {
            LandLevel prevLand;
            LandLevel currLand;

            for (int iCol = 0; iCol < GRIDSIZE; iCol++)
            {
                prevLand = oGrid[0][iCol].landType;
                prevLand = (int)prevLand > 1 ? 0 : prevLand;

                currLand = oGrid[0][iCol].landType;
                if ((int)currLand > 1)
                {
                    setNorthSouthRamp(0, iCol, currLand, gridColor);
                    currLand = LandLevel.lvl1;
                }

                // set start point at col 0
                debugCell.setStartPoint(0, iCol, prevLand, 0, 1);

                for (int iRow = 1; iRow < GRIDSIZE; iRow++)
                {
                    currLand = oGrid[iRow][iCol].landType;
                    if ((int)currLand > 1)
                    {
                        setNorthSouthRamp(iRow, iCol, currLand, gridColor);
                        currLand = LandLevel.lvl1;
                    }

                    if (prevLand != currLand)
                    {
                        connectLevels(iRow, iCol, gridColor, 0, 1);
                        debugCell.setEndPoint(iRow, iCol, prevLand, 0, 1);
                        setGridVerts(ref debugCell, gridColor);

                        // set new start point at new level
                        debugCell.setStartPoint(iRow, iCol, currLand, 0, 1);

                        prevLand = currLand;
                    }// if land type changed

                }// rows
                debugCell.setEndPoint(GRIDSIZE, iCol, currLand, 0, 1);
                setGridVerts(ref debugCell, gridColor);
            }// cols
        }

        private void connectLevels(int row, int col, Color gridColor, int rowScan, int colScan)
        {
            Debugcell connectPt = new Debugcell();

            connectPt.setStartPoint(row, col, LandLevel.lvl1, rowScan, colScan);
            connectPt.setEndPoint(row, col, LandLevel.lvl2, rowScan, colScan);
            setGridVerts(ref connectPt, gridColor);
        }
        private void setEastWestRamp(int row, int col, LandLevel landType, Color gridColor)
        {
            Debugcell oRamp = new Debugcell();
            switch (landType)
            {
                case LandLevel.downrampwest:
                    oRamp.setStartPoint(row, col, LandLevel.lvl1, 1, 0);
                    oRamp.setEndPoint(row, col + 1, LandLevel.lvl2, 1, 0);
                    setGridVerts(ref oRamp, gridColor);
                    break;
                case LandLevel.downrampeast:
                    oRamp.setStartPoint(row, col, LandLevel.lvl2, 1, 0);
                    oRamp.setEndPoint(row, col + 1, LandLevel.lvl1, 1, 0);
                    setGridVerts(ref oRamp, gridColor);
                    break;
            }
        }
        private void setNorthSouthRamp(int row, int col, LandLevel landType, Color gridColor)
        {
            Debugcell oRamp = new Debugcell();
            switch (landType)
            {
                case LandLevel.downrampsouth:
                    oRamp.setStartPoint(row, col, LandLevel.lvl1, 0, 1);
                    oRamp.setEndPoint(row + 1, col, LandLevel.lvl2, 0, 1);
                    setGridVerts(ref oRamp, gridColor);
                    break;
                case LandLevel.downrampnorth:
                    oRamp.setStartPoint(row, col, LandLevel.lvl2, 0, 1);
                    oRamp.setEndPoint(row + 1, col, LandLevel.lvl1, 0, 1);
                    setGridVerts(ref oRamp, gridColor);
                    break;
            }

        }
        private void setBlockLand(int row, int col)
        {
            Color clr = Color.DarkRed;
            setEastWestRamp(row, col, LandLevel.downrampeast, clr);
            setEastWestRamp(row, col, LandLevel.downrampwest, clr);
            setNorthSouthRamp(row, col, LandLevel.downrampnorth, clr);
            setNorthSouthRamp(row, col, LandLevel.downrampsouth, clr);
        }
        private void setGridVerts(ref Debugcell debugCell, Color gridColor)
        {
            oRenderGrid[iGridVertexIDx++] = new VertexPositionColor(debugCell.Start1, gridColor);
            oRenderGrid[iGridVertexIDx++] = new VertexPositionColor(debugCell.End1, gridColor);
            oRenderGrid[iGridVertexIDx++] = new VertexPositionColor(debugCell.Start2, gridColor);
            oRenderGrid[iGridVertexIDx++] = new VertexPositionColor(debugCell.End2, gridColor);
        }
        #endregion -------------------------------------------------------------------------

        /// <summary>
        /// returns an instance, but does not initialize the class.
        /// </summary>
        public static GameGrid Instance
        {
            get
            {
                return instance;
            }
        }
        /// <summary>
        /// retuns an instance and initializes the class if not initialized already
        /// </summary>
        /// <param name="device"></param>
        /// <param name="camera"></param>
        /// <returns></returns>
        public static GameGrid getInstance(GraphicsDevice device, Camera camera)
        {
            if (instance == null)
                instance = new GameGrid(device, camera);

            return instance;
        }
        /// <summary>
        /// registers the object onto the grid. This method does not check bounds.
        /// methods returns false if registation fails due to ocupied block.
        /// it is caller reponsibility to move the object back before frame is rendedred.
        /// caller should do the following:
        /// 1. cache old position and calculate new positions
        /// 2. call register
        /// 3. if registeration fails move back to cached position
        /// 4. else call unregister on old position
        /// </summary>
        /// <param name="gameObject"></param>
        /// <returns></returns>
        public bool registerObjectInstances(DrawableGlobalVar gameObject)
        {
            bool registrationSuccess = true;
            float X, Y;
            int XIdx, YIdx;
            Cell oCell;

            for (int iDx = 0; iDx < gameObject.oPosition.Length; iDx++)
            { 
                // only register active instances
                if (gameObject.bActive[iDx])
                {
                    X = gameObject.oPosition[iDx].X;
                    Y = gameObject.oPosition[iDx].Y;

                    // see page 10 for explaination of equation
                    XIdx = (int) Math.Floor((X / (float) GRIDUNITSIZE)) + GRIDSIZE;
                    YIdx = (int)Math.Floor((Y / (float)GRIDUNITSIZE)) + GRIDSIZE;

                    oCell = oGrid[XIdx][YIdx];

                    if (oCell.unitsRegistered != 4)
                    {
                        if (oCell.unit1 == null)
                        {
                            oCell.unit1 = gameObject;
                            oCell.unitsRegistered++;

                        }
                        else if (oCell.unit2 == null)
                        {
                            oCell.unit2 = gameObject;
                            oCell.unitsRegistered++;
                        }
                        else if (oCell.unit3 == null)
                        {
                            oCell.unit3 = gameObject;
                            oCell.unitsRegistered++;
                        }
                        else if (oCell.unit4 == null)
                        {
                            oCell.unit4 = gameObject;
                            oCell.unitsRegistered++;
                        }
                    }
                    else
                    {
                        registrationSuccess = false;
                    }
                }// is active
            }// for

            return registrationSuccess;
        }

        public void drawDebug()
        {
            oDevice.DepthStencilState = oDepthState;
            oEffect.VertexColorEnabled = true;
            oEffect.Projection = oCamera.Projection;
            oEffect.View = oCamera.View;
            oEffect.World = Matrix.Identity;
            EffectPass oPass = oEffect.CurrentTechnique.Passes[0];
            oPass.Apply();
            oDevice.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.LineList, oRenderGrid, 0, iGridVertexIDx / 2);
        }
    }
}
